package com.ego.search.service;

import com.ego.common.utils.NumberUtils;
import com.ego.common.utils.PageResult;
import com.ego.item.bo.SpuBo;
import com.ego.item.pojo.Brand;
import com.ego.item.pojo.Category;
import com.ego.item.pojo.Sku;
import com.ego.item.pojo.SpuDetail;
import com.ego.search.bo.SearchResult;
import com.ego.search.client.BrandClient;
import com.ego.search.client.CategoryClient;
import com.ego.search.client.GoodsClient;
import com.ego.search.client.SpecificationClient;
import com.ego.search.pojo.Goods;
import com.ego.search.bo.SearchRequest;
import com.ego.search.repository.GoodsRepository;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.netflix.discovery.converters.Auto;
import com.sun.org.apache.xpath.internal.operations.Number;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.histogram.InternalHistogram;
import org.elasticsearch.search.aggregations.bucket.terms.LongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.StringTerms;
import org.elasticsearch.search.aggregations.metrics.stats.InternalStats;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.ResultsExtractor;
import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.query.FetchSourceFilter;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

@Service
public class SearchService {

    @Autowired
    private CategoryClient categoryClient;
    @Autowired
    private GoodsClient goodsClient;
    @Autowired
    private GoodsRepository goodsRepository;
    @Autowired
    private ObjectMapper objectMapper = new ObjectMapper();

    @Autowired
    private BrandClient brandClient;

    @Autowired
    private SpecificationClient specificationClient;

    @Autowired
    private ElasticsearchTemplate elasticsearchTemplate;

    //操作数据转换,前端要什么东西,goods给什么东西
    //然后goods里面的东西是从spu拿到的嘛
    public Goods buildGoods(SpuBo spuBo) throws IOException {

        //先来操作一些简单的数据
        Goods goods = new Goods();
        goods.setId(spuBo.getId());
        //all 所有需要被搜索的信息，包含标题，分类，甚至品牌
        List<String> categoryNames = categoryClient.queryNameByIds(Arrays.asList(spuBo.getCid1(), spuBo.getCid2(), spuBo
                .getCid3())).getBody();//获取分类名字
        goods.setAll(spuBo.getTitle() + " " + categoryNames + " " + spuBo.getBname());
        goods.setSubTitle(spuBo.getSubTitle());
        goods.setBrandId(spuBo.getBrandId());
        goods.setCid1(spuBo.getCid1());
        goods.setCid2(spuBo.getCid2());
        goods.setCid3(spuBo.getCid3());
        goods.setCreateTime(spuBo.getCreateTime());
        //价格 List<Long> price
        //skusjson字符串
        //spuBo.getSkus()....不行,之前在做查询这个操作的时候,好像没有把它封装进去,so...重新带着spuId 去查一次skus
        List<Sku> skus = goodsClient.getCurSpuSkus(spuBo.getId()).getBody();//幸好有这个方法,好像是做更新sku数据回显的时候操作的
        List<Long> prices = new ArrayList<>();
        List<Map<String, Object>> skuLists = new ArrayList<>();//封装sku所需属性这操作着实自己没想到得
        if (skus != null) {//我的数据出了点问题,就是因为我添加商品的时候,sku没启用,然后就是空的,报了空指针,so..判断下
            skus.forEach(sku -> {
                prices.add(sku.getPrice());
                Map<String, Object> skuMap = new HashMap<>();
                skuMap.put("id", sku.getId());
                skuMap.put("title", sku.getTitle());
                skuMap.put("price", sku.getPrice());
                //取第一张图片
                skuMap.put("image", StringUtils.isBlank(sku.getImages()) ? "" :
                        StringUtils.split(sku.getImages(), ",")[0]);
                skuLists.add(skuMap);
            });
        }
        goods.setPrice(prices);
        goods.setSkus(objectMapper.writeValueAsString(skuLists));
        //下面的操作不熟练
        SpuDetail spuDetail = goodsClient.getCurSpu(spuBo.getId()).getBody();
        //提取公共的规格参数
        List<Map<String, Object>> genericSpecs = objectMapper.readValue(spuDetail.getSpecifications(),
                new TypeReference<List<Map<String, Object>>>() {
                });
        //特有的规格此参数值
        Map<String, Object> specialSpecs = objectMapper.readValue(spuDetail.getSpecTemplate(),
                new TypeReference<Map<String, Object>>() {
                });
        //过滤规格模板，把所有可搜索的信息保存到Map中
        Map<String, Object> specMap = new HashMap<>();
        String searchable = "searchable";
        String v = "v";
        String k = "k";
        String options = "options";
        genericSpecs.forEach(m -> {
            List<Map<String, Object>> params = (List<Map<String, Object>>) m.get("params");
            params.forEach(spe -> {
                if ((boolean) spe.get(searchable)) {
                    if (spe.get(v) != null) {
                        specMap.put(spe.get(k).toString(), spe.get(v));
                    } else if (spe.get(options) != null) {
                        specMap.put(spe.get(k).toString(), spe.get(options));
                    }
                }
            });
        });
        goods.setSpecs(specMap);
        return goods;
    }

    public SearchResult search(SearchRequest request) {
        //System.out.println(request);
        String key = request.getKey();
        // 判断是否有搜索条件，如果没有，直接返回null。不允许搜索全部商品
        if (StringUtils.isBlank(key)) {
            return null;
        }
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();

        //1、过key的字段去all分词里面找,应该是满足条件的都可以,所以分开用and
        //这儿将它提取出来,是后面规格参数聚合的时候用得,规格参数的聚合还得根据查询条件,不可能是聚合所有的商品撒
        QueryBuilder basicQuery = buildBasicQueryWithFilter(request);
        //QueryBuilder basicQuery = QueryBuilders.matchQuery("all", key).operator(Operator.AND);
        queryBuilder.withQuery(basicQuery);

        //2、通过sourceFilter设置返回的结果字段,我们只需要id、skus、subTitle
        queryBuilder.withSourceFilter(new FetchSourceFilter(
                new String[]{"id", "skus", "subTitle"}, null));

        //3、分页
        int page = request.getPage();
        int size = request.getSize();
        queryBuilder.withPageable(PageRequest.of(page - 1, size));

        //4、排序
        if (StringUtils.isNotBlank(request.getSortBy())) {
            queryBuilder.withSort(SortBuilders.fieldSort(request.getSortBy()).order(request.getDescending() ? SortOrder.DESC : SortOrder.ASC));
        }

        //5、分类和品牌的聚合,词条
        queryBuilder.addAggregation(AggregationBuilders.terms("categories").field("cid3"));
        queryBuilder.addAggregation(AggregationBuilders.terms("brands").field("brandId"));


        //单单用page的话,拿不到聚合的结果
        AggregatedPage<Goods> pageInfo = (AggregatedPage<Goods>) goodsRepository.search(queryBuilder.build());

        //获取聚合结果集
        List<Brand> brands = getBrandAggResult(pageInfo.getAggregation("brands"));
        List<Category> categories = getCategoryAggResult(pageInfo.getAggregation("categories"));

        //判断分类的个数,只有一个分类才做规格参数的聚合,这儿默认搜索第一个分类的参数
        List<Map<String, Object>> specs = new ArrayList<>();
        if (categories != null && categories.size() > 0) {
            //对规格参数进行聚合
            specs = getSpecAggResult(categories.get(0).getId(), basicQuery);
        }

        return new SearchResult(pageInfo.getTotalElements(),
                Long.valueOf(pageInfo.getTotalPages()), pageInfo.getContent(), categories, brands, specs);
    }

    /**
     * 改造基本查询,添加一下过滤的条件
     * 本方法纯copy的...
     *
     * @param searchRequest
     * @return
     */
    private QueryBuilder buildBasicQueryWithFilter(SearchRequest searchRequest) {
        BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();
//基本查询条件
        queryBuilder.must(QueryBuilders.matchQuery("all", searchRequest.getKey()).operator(Operator.AND));
//过滤条件构造器
        BoolQueryBuilder filterQueryBuilder = QueryBuilders.boolQuery();
//整理过滤条件
        Map<String, String> filter = searchRequest.getFilter();
        for (Map.Entry<String, String> entry : filter.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            String regex = "^(\\d+\\.?\\d*)-(\\d+\\.?\\d*)$";
            if (!"key".equals(key)) {
                if ("price".equals(key)) {
                    if (!value.contains("元以上")) {
                        String[] nums = StringUtils.substringBefore(value, "元").split("-");
                        filterQueryBuilder.must(QueryBuilders.rangeQuery(key).gte(Double.valueOf(nums[0]) *
                                100).lt(Double.valueOf(nums[1]) * 100));
                    } else {
                        String num = StringUtils.substringBefore(value, "元以上");
                        filterQueryBuilder.must(QueryBuilders.rangeQuery(key).gte(Double.valueOf(num) * 100));
                    }
                } else {
                    if (value.matches(regex)) {
                        Double[] nums = NumberUtils.searchNumber(value, regex);
//数值类型进行范围查询 lt:小于 gte:大于等于
                        filterQueryBuilder.must(QueryBuilders.rangeQuery("specs." +
                                key).gte(nums[0]).lt(nums[1]));
                    } else {
//商品分类和品牌要特殊处理
                        if (key != "cid3" && key != "brandId") {
                            key = "specs." + key + ".keyword";
                        }
//字符串类型，进行term查询
                        filterQueryBuilder.must(QueryBuilders.termQuery(key, value));
                    }
                }
            } else {
                break;
            }
        }
//添加过滤条件
        queryBuilder.filter(filterQueryBuilder);
        return queryBuilder;
    }


    /**
     * 根据查询条件聚合规格参数
     * 以及解析结果集
     * 返回数据
     *
     * @param id         根据类别id获取相应的规格参数
     * @param basicQuery 基本查询条件
     * @return
     */
    private List<Map<String, Object>> getSpecAggResult(Long id, QueryBuilder basicQuery) {
        List<Map<String, Object>> result = new ArrayList<Map<String, Object>>();
        //查询要聚合的规格参数
        String specsJson = specificationClient.querySpecificationByCategoryId(id).getBody();
        List<Map<String, Object>> specList = null;
        try {
            specList = objectMapper.readValue(specsJson, new TypeReference<List<Map<String, Object>>>() {
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
        //过滤出可以搜索的规格参数名称
        //存储字符型规格参数 k
        Set<String> strSpec = new HashSet<>();
        //用来保存数值规格参数名及单位 k ,unit
        Map<String, String> numSpec = new HashMap<>();
        specList.forEach((spec) -> {
            String k = "k";
            String unit = "unit";
            //param得到的又是一个list,里面存放的是键值对
            if (spec.get("params") != null) {
                ((List<Map<String, Object>>) spec.get("params")).forEach((param) -> {
                    Boolean searchable = (Boolean) param.get("searchable");
                    if (searchable != null && searchable) {//首先是可以提供搜索的参数
                        //区分下是不是数值类型的,在数据库param中有numerical
                        if (param.get("numerical") != null && (Boolean) param.get("numerical")) {
                            numSpec.put((String) param.get(k), (String) param.get(unit));
                        } else {
                            strSpec.add((String) param.get(k));
                        }
                    }
                });
            }

        });
        //数字型需要阶梯聚合,因此需要查询到对应数字型参数的间隔,以及单位
        //存储数字型参数的间隔(参数名,间隔)
        Map<String, Double> intervalMap = getNumInterval(basicQuery, numSpec);


        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        queryBuilder.withQuery(basicQuery);

        //聚合
        //词条聚合,字符参数
        strSpec.forEach((k) -> {
            queryBuilder.addAggregation(AggregationBuilders.terms(k).field("specs." + k + ".keyword"));
        });
        //histogram聚合,数值参数聚合
        numSpec.forEach((k, unit) -> {
            //System.out.println(k + "===================" + intervalMap.get(k));
            if (intervalMap.get(k) != null && intervalMap.get(k) > 0) {
                queryBuilder.addAggregation(AggregationBuilders.histogram(k).field("specs." + k).interval(intervalMap.get(k)));
            }
        });

        //聚合查询,并解析聚合结果集
        //这儿每一个聚合我取的名字是k的值,在kibana里面,是这样子
        // k的值:{"buckets":[{"key":"","doc_count": 136}]}
        Map<String, Aggregation> resultMap = elasticsearchTemplate.query(queryBuilder.build(), (searchResponse -> {
            if (searchResponse.getAggregations() != null) {//可能为空,因为我自己添加的不规范的数据,报了个空指针
                return searchResponse.getAggregations().getAsMap();
            }
            return null;
        }));
        //解析结果集
        //解析字符参数的结果集合
        strSpec.forEach(k -> {
            StringTerms aggregation = (StringTerms) resultMap.get(k);
            Map<String, Object> map = new HashMap<>();
            map.put("k", k);
            List<String> options = aggregation.getBuckets().stream().map(bucket -> {
                return bucket.getKeyAsString();
            }).collect(Collectors.toList());
            map.put("options", options);
            result.add(map);
        });
        //解析数值参数的结果集
        numSpec.forEach((k, unit) -> {
            InternalHistogram aggregation = (InternalHistogram) resultMap.get(k);
            Map<String, Object> map = new HashMap<>();
            map.put("k", k);
            List<String> options = null;
            if (aggregation!= null) {
                options = aggregation.getBuckets().stream().map(bucket -> {
                    //0-400 400-800 800-1200
                    Double begin = (Double) bucket.getKey();
                    Double end = begin + intervalMap.get(k);
                    if (NumberUtils.isInt(begin) && NumberUtils.isInt(end)) {
                        return begin + "-" + end;
                    } else {
                        return NumberUtils.scale(begin, 2) + "-" + NumberUtils.scale(end, 2);
                    }
                }).collect(Collectors.toList());
            }

            map.put("options", options);
            map.put("unit", unit);
            result.add(map);
        });
        //返回
        return result;
    }

    /**
     * 计算各个数值参数的间隔
     *
     * @param basicQuery
     * @param numSpec
     * @return
     */
    private Map<String, Double> getNumInterval(QueryBuilder basicQuery, Map<String, String> numSpec) {
        Map<String, Double> result = new HashMap<>();
        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
        queryBuilder.withQuery(basicQuery);
        numSpec.forEach((k, unit) -> {
            //添加聚合条件
            queryBuilder.addAggregation(AggregationBuilders.stats(k).field("specs." + k));
        });
        //查询并解析结果
        Map<String, Aggregation> queryMap = elasticsearchTemplate.query(queryBuilder.build(), (searchResponse -> {
            if (searchResponse.getAggregations() != null) {//可能为空,因为我自己添加的不规范的数据,报了个空指针
                return searchResponse.getAggregations().getAsMap();
            }
            return null;
        }));

        if (queryMap != null && queryMap.size() != 0) {//也报了个空指针,就是我乱填数据的问题,丫的
            queryMap.forEach((k, unit) -> {
                InternalStats aggregation = (InternalStats) queryMap.get(k);
                Double interval = NumberUtils.getInterval(aggregation.getMin(), aggregation.getMax(), aggregation.getSum());
                result.put(k, interval);
            });
        }
        return result;
    }

    /**
     * 解析分类的聚合结果集
     *
     * @param categories
     * @return
     */
    private List<Category> getCategoryAggResult(Aggregation categories) {
        LongTerms categoryAgg = (LongTerms) categories;
        List<Long> categoryIds = categoryAgg.getBuckets().stream().map(bucket -> {
            return bucket.getKeyAsNumber().longValue();
        }).collect(Collectors.toList());
        List<Category> categoryList = categoryClient.queryCategoryListByIds(categoryIds);
        return categoryList;
    }

    /**
     * 解析品牌的聚合结果集
     *
     * @param brands
     * @return
     */
    private List<Brand> getBrandAggResult(Aggregation brands) {
        // 因为是利用Long类型字段来进行的term聚合，所以结果要强转为LongTerms类型
        LongTerms brandAgg = (LongTerms) brands;
        //获取桶,然后拿到桶中的key获取到brand,然后收集到一个list中
        return brandAgg.getBuckets().stream().map(bucket -> {
            return brandClient.queryBrandById(bucket.getKeyAsNumber().longValue()).getBody();
        }).collect(Collectors.toList());

    }
}
